// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace SnapObjects.Data
{
    internal class PropertyAccessorFactory
    {
        private readonly ConcurrentDictionary<Type, IReadOnlyDictionary<string, IPropertyAccessor>> _propertyAccessors;

        private PropertyAccessorFactory()
        {
            _propertyAccessors = new ConcurrentDictionary<Type, IReadOnlyDictionary<string, IPropertyAccessor>>();
        }

        public static PropertyAccessorFactory Instance { get; } = new PropertyAccessorFactory();

        public IReadOnlyDictionary<string, IPropertyAccessor> GetPropertyAccessors(Type modelType)
        {

            if (!_propertyAccessors.TryGetValue(modelType, out var propertyAccessors))
            {
                var modelMeta = ModelMetaManager.GetModelMeta(modelType);
                var tempAccessors = new ConcurrentDictionary<string, IPropertyAccessor>(StringComparer.OrdinalIgnoreCase);

                foreach (var item in modelMeta.PropertyMap)
                {
                    var accessor = this.Create(modelType, (PocoPropertyMeta)item.Value);
                    tempAccessors.TryAdd(item.Value.Name, accessor);
                }

                _propertyAccessors.TryAdd(modelType, tempAccessors);

                return tempAccessors;
            }

            return propertyAccessors;
        }

        private IPropertyAccessor Create(Type modelType, PocoPropertyMeta property)
        {
            var methodInfo = typeof(PropertyAccessorFactory).GetMethod(nameof(this.CreateInternal),
                BindingFlags.NonPublic | BindingFlags.Instance);

            var method = methodInfo.MakeGenericMethod(modelType, property.DataType);

            return (IPropertyAccessor)method.Invoke(Instance, new object[] { modelType, property });
        }

        private IPropertyAccessor CreateInternal<TModel, TValue>(Type modelType, PocoPropertyMeta property)
        {
            return new PropertyAccessor<TModel, TValue>(
                property,
                this.CreateGetter<TModel, TValue>(modelType, property),
                this.CreateSetter<TModel, TValue>(modelType, property));
        }

        private Func<TModel, TValue> CreateGetter<TModel, TValue>(Type modelType, PocoPropertyMeta property)
        {
            var memberInfo = this.GetPropertiesInHierarchy(modelType, property.PropertyInfo.Name)
                    .FirstOrDefault(p => p.GetMethod != null);

            if (memberInfo != null)
            {
                var modelParameter = Expression.Parameter(typeof(TModel), "model");

                return Expression.Lambda<Func<TModel, TValue>>(
                    Expression.MakeMemberAccess(modelParameter, memberInfo),
                    modelParameter).Compile();
            }
            else
            {
                return null;
            }
        }

        private Action<TModel, TValue> CreateSetter<TModel, TValue>(Type modelType, PocoPropertyMeta property)
        {
            var memberInfo = this.GetPropertiesInHierarchy(modelType, property.PropertyInfo.Name)
                    .FirstOrDefault(p => p.SetMethod != null);

            if (memberInfo != null)
            {
                var modelParameter = Expression.Parameter(typeof(TModel), "model");
                var valueParameter = Expression.Parameter(typeof(TValue), "value");

                return Expression.Lambda<Action<TModel, TValue>>(
                        Expression.Assign(Expression.MakeMemberAccess(modelParameter, memberInfo),
                            valueParameter), modelParameter, valueParameter).Compile();
            }
            else
            {
                return null;
            }
        }

        private IEnumerable<PropertyInfo> GetPropertiesInHierarchy(Type type, string name)
        {
            do
            {
                var typeInfo = type.GetTypeInfo();
                var propertyInfo = typeInfo.GetDeclaredProperty(name);
                if (propertyInfo != null
                    && !(propertyInfo.GetMethod ?? propertyInfo.SetMethod).IsStatic)
                {
                    yield return propertyInfo;
                }
                type = typeInfo.BaseType;
            }
            while (type != null);
        }
    }
}
